package cn.tedu.charging.order.service.impl;

import cn.tedu.charging.common.pojo.JsonResult;
import cn.tedu.charging.common.pojo.param.GunStatusUpdateParam;
import cn.tedu.charging.common.pojo.vo.StationInfoVO;
import cn.tedu.charging.common.pojo.vo.UserInfoVo;
import cn.tedu.charging.common.utils.JsonUtils;
import cn.tedu.charging.order.common.ChargingConstants;
import cn.tedu.charging.order.common.GunStatusConstants;
import cn.tedu.charging.order.common.MqttConstants;
import cn.tedu.charging.order.feign.DeviceClient;
import cn.tedu.charging.order.feign.UserClient;
import cn.tedu.charging.order.mqtt.MqttProducer;
import cn.tedu.charging.order.pojo.dto.ChargingDto;
import cn.tedu.charging.order.pojo.param.OrderAddParam;
import cn.tedu.charging.order.quartz.DeviceCheckJob;
import cn.tedu.charging.order.rabbitmq.RabbitMQProducer;
import cn.tedu.charging.order.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.Random;
//静态导入
//import static cn.tedu.charging.order.common.MqttConstants.TOPIC_START_CHARGING_PREFIX;


@Slf4j
@Service
public class OrderServiceImpl  implements OrderService {
    /**
     * 创建订单
     * 1. 生成订单号 （生产的逻辑）
     * 2. 获取 用户信息（车辆id） (微服务架构  订单服务调用 用户服务 服务之间通信)
     * 3. 获取 服务信息（场站信息和设备信息 充电站id,运营商id...） (微服务架构  订单服务调用 设备服务 服务之间通信)
     * 4. 修改充电枪的状态 为 使用中  (微服务架构  订单服务调用 设备服务 服务之间通信)
     * 5. 给设备发送开始充电指令 （物联网 订单服务 调用 充电桩设备 服务于设备之间通信）
     * @param orderAddParam
     * @return
     */
    @Override
    public String createOrder(OrderAddParam orderAddParam) {
        log.debug("创建订单:{}",orderAddParam);
        //1.生成订单号
        log.debug("创建订单-生成订单号");
        String orderNo = getOrderNo();
        log.debug("创建订单-生成订单号:{}",orderNo);
        //2.获取 用户信息 (车辆id)
        Integer userId = orderAddParam.getUserId();
        log.debug("创建订单-获取用户信息-入参:{}",userId);
        UserInfoVo userInfo = getUserInfoByOpenFeign(userId);
        log.debug("创建订单-获取用户信息-入参:{},出参:{}",userId,userInfo);
        //3. 获取 服务信息（场站信息和设备信息 充电站id,运营商id...）
        log.debug("创建订单-获取场站信息-入参:{}",orderAddParam.getGunId());
        StationInfoVO stationInfoVO =getStationInfo(orderAddParam.getGunId());
        log.debug("创建订单-获取场站信息-入参:{},出参:{}",orderAddParam.getGunId(),stationInfoVO);
        //4. 修改充电枪的状态 为 使用中 // 1 使用中 魔数 应该定义为常量或者枚举
        log.debug("创建订单-修改充电枪状态-入参:{},{}",orderAddParam.getGunId(), GunStatusConstants.GUN_STATUS_USING);
        //失败 李四 账户 加 100 9091
        Boolean result = updateGunStatus(orderAddParam.getGunId(),GunStatusConstants.GUN_STATUS_USING);
        log.debug("创建订单-修改充电枪状态-入参:{},出参:{}",orderAddParam.getGunId(),result);
        //5. 给设备发送开始充电指令 （物联网 订单服务 调用 充电桩设备 服务于设备之间通信）
        startCharging(orderNo,orderAddParam.getPileId(),orderAddParam.getGunId(),userId);
        //6.创建设备检查任务 用来检查设备无响应
        log.debug("创建订单-创建设备检查任务-入参:{}",orderNo);
        try {
            DeviceCheckJob deviceCheckJob = new DeviceCheckJob(orderNo);
        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }
        log.debug("创建订单-创建设备检查任务成功-入参:{}",orderNo);
        log.debug("创建订单-给消息队列发送有生存时间的消息,实现延迟消息,用来处理超时订单 <<充电过程中的设备故障>>:{}",orderNo);
        Boolean success = sendMessage(orderNo);
        log.debug("创建订单-给消息队列发送有生存时间的消息,实现延迟消息,用来处理超时订单 <<充电过程中的设备故障>>:{},结果:{}",orderNo,success);
        return orderNo;
    }

    @Autowired
    private RabbitMQProducer rabbitMQProducer;

    /**
     * 给RabbitMq发送消息 用来处理超时订单
     * @param orderNo
     */
    private Boolean sendMessage(String orderNo){
        //todo 如果想传多个字段信息可以封装为一个dto
        rabbitMQProducer.send(orderNo);
        return true;
    }

    @Autowired
    private MqttProducer mqttProducer;
    /**
     * 开始充电
     * 给设备发送开始充电指令
     */
    private void startCharging(String orderNo,Integer pileId,Integer gunId,Integer userId){
        //1> Topic的设计
        //枪维度 枪_Id
        //桩维度 桩_id
        // 桩维度 和 枪维度 都可以
        //用桩维度 的topic的数量会比 枪维度的topic数量理论上会 节省资源
        //用桩维度 消息体 应该包含 枪id
        String topic = MqttConstants.TOPIC_START_CHARGING_PREFIX + pileId;
        //2> 消息的设计
        //业务目标 订单服务发送开始充电指令到设备
        //开始充电指令是一个消息 消息对于设备来说 是 入参
        //思考 接口的入参 该如何设计
        //业务的目标 业务是什么？业务如果缺少了什么参数，会导致业务无法开始，这些参数就是入参的核心参数
        //首先明确业务目标
        //订单服务-->开始充电-->
        // 给特定的设备(设备的信息)-->
        // 发送特定的指令(开始充电指令)-->
        // 给具体的用户的具体的订单(订单的唯一标识订单号)
        //订单信息的唯一标识 订单号 设备信息 枪id 告诉桩 用哪个枪 目标 给哪个用户的哪个订单 开始充电
        //1> 自定义协议
        // |订单号|桩id|枪id|start| //自定义格式 按照分割符号分割
        //String message = "|1000_333_112233|666|6661|start|"
        //String 类 的常用方法 要熟悉
        //String[] arrays = message.split("|")
        //2> json 格式
        ChargingDto chargingDto = new ChargingDto();
        chargingDto.setOrderNo(orderNo);
        chargingDto.setPileId(pileId);
        chargingDto.setGunId(gunId);
        chargingDto.setUserId(userId);
        chargingDto.setMsg(ChargingConstants.START_CHARGING);
        log.debug("订单服务发送开始充电指令到设备Topic:{},消息:{}",topic,chargingDto);
        //java对象怎么转换为Json
        //消息的序列化 Spring-Boot MVC 已经帮我们自动做好了 对象转json json转对象
        //Jackson json库
        //https://cloud.tencent.com/developer/article/2242912 json库 Gson FastJson ...
        String message = JsonUtils.toJson(chargingDto);
        log.debug("订单服务发送开始充电指令到设备Topic:{},消息对象:{},消息json:{}",
                topic,chargingDto,message);
        Boolean success = mqttProducer.send(topic, message);
        log.debug("订单服务发送开始充电指令到设备Topic:{},消息对象:{},消息json:{},发送结果:{}",
                topic,chargingDto,message,success);
    }
    @Autowired
    private DeviceClient deviceClient;
    /**
     * 通过 deviceClient 调用设备服务 更新枪的状态
     * @param gunId
     * @param status
     * @return
     */
    public Boolean updateGunStatus(Integer gunId,Integer status){
        GunStatusUpdateParam param = new GunStatusUpdateParam();
        param.setGunId(gunId);
        param.setStatus(status);
        JsonResult<Boolean> result =deviceClient.updateGunStatus(param);
        if (result != null) {
            return result.getData();
        }
        return false;
    }
    /**
     * 通过deviceClient 通过设备服务获取场站信息
     * @param gunId
     * @return
     */
    private StationInfoVO getStationInfo(Integer gunId){
        JsonResult<StationInfoVO> stationInfo = deviceClient.getStationInfo(gunId);
        if (stationInfo != null) {
            return stationInfo.getData();
        }
        return null;
    }

    @Autowired
     private RestTemplate restTemplate;
    /**
     * 调用用户服务获取用户车辆信息
     * @param userId
     * @return
     */
//    private UserInfoVo getUserInfo(Integer userId){
//        String url = getRandomUrl();
//        log.debug("通过用户服务获取用户信息接口地址:{}",url);
//        //String url = "http://localhost:8080/user/info/8";
//
//        ParameterizedTypeReference<JsonResult<UserInfoVo>>
//                repType =
//                new ParameterizedTypeReference<JsonResult<UserInfoVo>>() {};
//
//        ResponseEntity<JsonResult<UserInfoVo>> entity =
//                restTemplate.exchange(
//                        url, HttpMethod.GET,null,
//                        repType,userId);
//
//        JsonResult<UserInfoVo> body = entity.getBody();
//      return body.getData();
//    }


    /**
     * 订单服务通过open-feign 服务之间通信工具 和 注册中心 和负载均衡
     * 调用用户服务 获取用户车辆信息
     * @param userId
     * @return
     */
    @Autowired
    private UserClient userClient;
    private UserInfoVo getUserInfoByOpenFeign(Integer userId){
        JsonResult<UserInfoVo> userInfo = userClient.getUserInfo(userId);
        UserInfoVo data = userInfo.getData();
        return data;
    }

    /**
     * 自己手写一个随机负载均衡算法
     * 1 把用户的服务的 URL 放入到数组
     * 2 通过JavaAPI Random 生成一个随机数
     * 3 把随机数作为 数组的下标 index ，返回对应的URL
     * @return
     */
    private String getRandomUrl(){
        String url = "http://localhost:8080/user/info/{1}";
        String url1 = "http://localhost:8081/user/info/{1}";
        String url2 = "http://localhost:8082/user/info/{1}";
        String[] urls = new String[]{url,url1,url2};
        Random random = new Random();
        int rIndex = random.nextInt(urls.length);
        return urls[rIndex];
    }

    /**
     * 生成订单号
     * 唯一性
     *  1 UUID 随机并且唯一
     *  2 自增的数据库主键  递增 不重复 唯一  分库分表下不能保证唯一 会重复
     *
     * 全局唯一性：不能出现重复的ID号，既然是唯一标识，这是最基本的要求。
     * 趋势递增：在MySQL InnoDB引擎中使用的是聚集索引，由于多数RDBMS使用B-tree的数据结构来存储索引数据，在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
     * 单调递增：保证下一个ID一定大于上一个ID，例如事务版本号、IM增量消息、排序等特殊需求。
     * 信息安全：如果ID是连续的，恶意用户的扒取工作就非常容易做了，直接按照顺序下载指定URL即可；如果是订单号就更危险了，竞对可以直接知道我们一天的单量。所以在一些应用场景下，会需要ID无规则、不规则。
     *
     * 分布式id生成器 分布式场景下 生成唯一的id
     * 雪花算法  https://www.jianshu.com/p/1af94260664a  twitter的snowflake算法  (分布式id生成器)
     * 美团leaf https://tech.meituan.com/2017/04/21/mt-leaf.html
     *
     *
     * MVP 业务刚刚开始 先自己生成
     *
     *  100000 + 随机数  + 时间戳
     * @return
     */
    private String getOrderNo(){
        //1 前缀
        String start = "100000";
        Random random = new Random();
        //2 生成10000 之内的随机数
        int r = random.nextInt(10000);
        //3 时间戳
        long now = System.currentTimeMillis();
        return start + "_" + r  + "_"  + now;
    }
}
